package adfc2;

import adfc2.actions.StepAction;
import adfc2.core.*;
import adfc2.entities.*;
import adfc2.items.Pickaxe;
import adfc2.items.Stone;
import adfc2.render3d.TextureReference;
import adfc2.render3d.mesh.Meshes;
import adfc2.render3d.tex.TiledTexture;
import com.sun.opengl.util.GLUT;
import com.sun.opengl.util.texture.Texture;
import com.sun.opengl.util.texture.TextureIO;

import javax.media.opengl.*;
import javax.media.opengl.glu.GLU;
import java.awt.*;
import java.awt.event.*;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

import static adfc2.core.World.*;
import static javax.media.opengl.GL.*;

public class Renderer3D extends Renderer {
    public static final int DDS_FLAT = 0;
    public static final int DDS_BOX_START = 1;
    public static final int DDS_BOX_END = 5;
    public static double[][] ddstex = {
            {1, 0}, {0, 0}, {0, 1}, {1, 1}
    };
    public final static float[][][] dds = {
            {{1, 1, 0}, {0, 1, 0}, {0, 0, 0}, {1, 0, 0}, {0, 0, 1}},//bottom quad facing up
            {{1, 0, 1}, {0, 0, 1}, {0, 0, 0}, {1, 0, 0}, {0, -1, 0}},//front quad facing front
            {{0, 0, 1}, {0, 1, 1}, {0, 1, 0}, {0, 0, 0}, {-1, 0, 0}},//left quad facing left
            {{1, 1, 1}, {1, 0, 1}, {1, 0, 0}, {1, 1, 0}, {1, 0, 0}},//right quad facing right
            {{0, 1, 0}, {0, 1, 1}, {1, 1, 1}, {1, 1, 0}, {0, 1, 0}},//back quad facing back
            {{1, 1, 1}, {0, 1, 1}, {0, 0, 1}, {1, 0, 1}, {0, 0, 1}} //top face facing up
    };
    public static final int TEXTURE_COLS = 8;
    GLU glu = new GLU();
    GLUT glut = new GLUT();
    float[] cameraTarget = {WORLD_WIDTH / 2, WORLD_WIDTH / 2, 0};
    float cameraDistance = 5;
    float[] cameraRotation = {30, 0, 0};
    float[] cameraSpeed = {0, 0, 0};
    float[] cameraRSpeed = {0, 0, 0};
    double[] gridModelMatrix = new double[16];
    double[] gridProjectionMatrix = new double[16];
    int[] viewportParams = new int[4];
    GL gl;
    @SuppressWarnings({"FieldCanBeLocal"})
    private int mouseX;
    @SuppressWarnings({"FieldCanBeLocal"})
    private int mouseY;
    final GLCanvas canvas;
    private int mouseCellXY;
    private Meshes meshes;

    private Texture curTex;
    private Set<Integer> curGlClientParams = new HashSet<Integer>();
    private Set<Integer> curGlParams = new HashSet<Integer>();
    private int curTexIdx;
    private int curTexWidth;
    private int curTexHeight;
    private TextureReference[] textureParams = new TextureReference[10];
    private TiledTexture tiles4items;
    private TiledTexture tiles4materials;

    public Renderer3D(World world, Container container, IRendererCallback callback) {
        super(container, world, callback);
        container.setLayout(new BorderLayout());
        GLCapabilities caps = new GLCapabilities();
        caps.setDoubleBuffered(true);
        caps.setStencilBits(1);
        canvas = new GLCanvas(caps);
        canvas.setPreferredSize(new Dimension(1000, 1000));
        container.add(canvas, BorderLayout.CENTER);

        initGui();
    }


    private int unproject(int mouseX, int mouseY) {
        double[] xyz1 = new double[4];
        glu.gluUnProject(mouseX, canvas.getHeight() - mouseY, 0, gridModelMatrix, 0, gridProjectionMatrix, 0, viewportParams, 0, xyz1, 0);
        double[] xyz2 = new double[4];
        glu.gluUnProject(mouseX, canvas.getHeight() - mouseY, 3, gridModelMatrix, 0, gridProjectionMatrix, 0, viewportParams, 0, xyz2, 0);
        // Ищем пересечение линии, проходящей через xyz1 и xyz2 с плоскостью z=0
        // (x1-x)/(z1-0) = (x2-x1)/(z2-z1)
        double x1 = xyz1[0];
        double y1 = xyz1[1];
        double z1 = xyz1[2];
        double x2 = xyz2[0];
        double y2 = xyz2[1];
        double z2 = xyz2[2];
        double x = x1 - (x2 - x1) * z1 / (z2 - z1);
        double y = y1 - (y2 - y1) * z1 / (z2 - z1);
        return mkxy((int) x, (int) y);
    }

    @SuppressWarnings({"UnusedParameters"})
    private void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
        gl = drawable.getGL();
        gl.glViewport(0, 0, width, height);
        gl.glMatrixMode(GL_PROJECTION);
        gl.glLoadIdentity();
        glu.gluPerspective(45, ((double) width) / height, 0.1, 1000);
        //gl.glFrustum(0,width,height,0,-1.0,1.0);
        //gl.glOrtho(0, width, height, 0, -1.0, 1.0);
        gl.glMatrixMode(GL_MODELVIEW);
        gl.glLoadIdentity();
    }

    private void displayChanged(GLAutoDrawable drawable, boolean modeChanged, boolean deviceChanged) {
        // la la la lets pretend nothing happened
    }

    private void setupGl() {
        curGlParams.clear();
        curGlClientParams.clear();
        ensureEnabled(GL_LIGHTING, true);
        ensureEnabled(GL_COLOR_MATERIAL, true);
        ensureEnabled(GL_LIGHT0, true);
        ensureEnabled(GL_DEPTH_TEST, true);
        ensureEnabled(GL_CCW, true);
        ensureEnabled(GL_CULL_FACE, true);
        ensureEnabled(GL_NORMALIZE, true);
        gl.glDisable(GL_TEXTURE_2D);
        curTex = null;
        curTexHeight = 1;
        curTexWidth = 1;
        curTexIdx = 0;

        gl.glLightModelfv(GL_LIGHT_MODEL_AMBIENT, new float[]{0, 0, 0, 1}, 0);
        gl.glColorMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE);

        tiles4items.texture.bind();
        gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        tiles4materials.texture.bind();
        gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

        gl.glLightfv(GL_LIGHT0, GL_DIFFUSE, new float[]{1, 1, 1}, 0);
        gl.glLightfv(GL_LIGHT0, GL_AMBIENT, new float[]{0.1f, 0.1f, 0.1f}, 0);

        gl.glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        gl.glMatrixMode(GL_TEXTURE);
        gl.glLoadIdentity();
        gl.glMatrixMode(GL_MODELVIEW);
        gl.glLoadIdentity();
    }

    private void display(GLAutoDrawable drawable) {
        gl = drawable.getGL();
        setupGl();

        //cameraRx++;


        gl.glRotated(-90, 1, 0, 0); // z: вверх, x: влево, y: вглубь

        gl.glLightfv(GL_LIGHT0, GL_POSITION, new float[]{0, 0, 0, 1}, 0);


        gl.glTranslated(0, cameraDistance, 0);
        gl.glRotated(cameraRotation[1], 0, 1, 0);
        gl.glRotated(cameraRotation[0], 1, 0, 0);
        gl.glRotated(cameraRotation[2], 0, 0, 1);
        gl.glTranslated(-cameraTarget[0], -cameraTarget[1], -cameraTarget[2]);

        // axis
        TextureReference.NO_TEXTURE.activate(this);
        gl.glBegin(GL_LINES);
        {
            gl.glColor3d(1, 0, 0);
            gl.glVertex3d(0, 0, 0);
            gl.glVertex3d(100, 0, 0);
            gl.glColor3d(0, 1, 0);
            gl.glVertex3d(0, 0, 0);
            gl.glVertex3d(0, 100, 0);
            gl.glColor3d(0, 0, 1);
            gl.glVertex3d(0, 0, 0);
            gl.glVertex3d(0, 0, 100);
            gl.glColor3d(0, 1, 1);
            gl.glVertex3d(0, 0, 0);
            gl.glVertex3d(-100, 0, 0);
            gl.glColor3d(1, 0, 1);
            gl.glVertex3d(0, 0, 0);
            gl.glVertex3d(0, -100, 0);
            gl.glColor3d(1, 1, 0);
            gl.glVertex3d(0, 0, 0);
            gl.glVertex3d(0, 0, -100);
        }
        gl.glEnd();
        // blocks
        renderBlocks();
        // entities
        for (Entity entity : world.entities()) {
            int x = xy2x(entity.getXY());
            int y = xy2y(entity.getXY());
            gl.glPushMatrix();
            gl.glTranslatef(x, y, 0);
            if (entity instanceof Tree) {
                textureParams[0] = tiles4materials.tiles[Material.OAK_TREE.ordinal()];
                meshes.TREE_MESH.render(this);
            } else if (entity instanceof Creature) {
                renderCreature((Creature) entity);
            } else if (entity instanceof DroppedItem) {
                renderDroppedItem((DroppedItem) entity);
            } else if (entity instanceof Building) {
                renderBuilding((Building) entity);
            } else if (entity instanceof BuildingPart) {
                // la la la nothing to do here
            } else {
                throw new UnsupportedOperationException(entity + " not supported");
            }
            gl.glPopMatrix();
        }

        // grid
        TextureReference.NO_TEXTURE.activate(this);
        gl.glGetDoublev(GL_MODELVIEW_MATRIX, gridModelMatrix, 0);
        gl.glGetDoublev(GL_PROJECTION_MATRIX, gridProjectionMatrix, 0);
        gl.glGetIntegerv(GL_VIEWPORT, viewportParams, 0);
        gl.glDisable(GL_LIGHTING);
        gl.glBegin(GL_LINES);
        {
            gl.glColor3d(1, 1, 1);
            for (int i = 0; i <= WORLD_WIDTH; i++) {
                gl.glVertex3d(i, 0, 0.01);
                gl.glVertex3d(i, WORLD_WIDTH, 0.01);
                gl.glVertex3d(0, i, 0.01);
                gl.glVertex3d(WORLD_WIDTH, i, 0.01);
            }
        }
        gl.glEnd();
        gl.glColor3f(1, 1, 0);

        gl.glPushMatrix();
        int mcx = xy2x(mouseCellXY);
        int mcy = xy2y(mouseCellXY);
        gl.glTranslatef(mcx, mcy, 0);
        gl.glDisable(GL_DEPTH_TEST);
        gl.glBegin(GL_LINES);
        {
            for (int i = DDS_BOX_START; i <= DDS_BOX_END; i++) {
                for (int j = 0; j < 4; j++) {
                    gl.glVertex3fv(dds[i][j], 0);
                    gl.glVertex3fv(dds[i][(j + 1) % 4], 0);
                }
            }
        }
        gl.glEnd();
        gl.glPopMatrix();
    }

    private void renderBuilding(Building entity) {
        textureParams[0] = tiles4materials.tiles[entity.components[0].material().ordinal()];
        if (entity instanceof MasonWorkshop) {
            meshes.MASON_WORKSHOP_MESH.render(this);
        }
    }

    private void renderDroppedItem(DroppedItem entity) {
        gl.glColor3f(1, 1, 1);
        Item item = entity.item;
        TextureReference texture;
        if (item instanceof Pickaxe) {
            texture = tiles4items.tiles[1];
        } else if (item instanceof Stone) {
            texture = tiles4materials.tiles[((Stone) item).material.ordinal()];
        } else {
            texture = tiles4items.tiles[1];
        }
        textureParams[0] = texture;
        meshes.DROPPED_ITEM_MESH.render(this);
    }

    private void renderCreature(Creature entity) {
        if (entity instanceof Dwarf) {
            ensureTextureSelected(null);
            if (entity.action != null && entity.action instanceof StepAction) {
                StepAction stepAction = (StepAction) entity.action;
                int dx = xy2x(stepAction.toxy) - xy2x(entity.getXY());
                int dy = xy2y(stepAction.toxy) - xy2y(entity.getXY());
                double progress = stepAction.estimateProgress();
                gl.glTranslated(dx * progress, dy * progress, 0);
            }
            gl.glColor3f(1, 0.5f, 0);
            gl.glTranslatef(0, 0.4f, 0);
            gl.glScalef(1, 0.2f, 1);
            //renderQuads(Meshes.GLYPH_AT);
            meshes.GLYPH_AT.render(this);
        } else {
            gl.glColor3f(1, 1, 1);
            gl.glTranslatef(0.5f, 0.5f, 0.5f);
            glut.glutSolidSphere(0.5, 8, 8);
        }
    }


    private void renderBlocks() {
        //   gl.glEnable(GL_LIGHTING);
        tiles4materials.full.activate(this);
        gl.glColor3f(1, 1, 1);
        gl.glBegin(GL_QUADS);
        {
            for (int x = 0; x < WORLD_WIDTH; x++) {
                for (int y = 0; y < WORLD_WIDTH; y++) {
                    gl.glPushMatrix();
                    gl.glTranslatef(x, y, 0);
                    int xy = mkxy(x, y);
                    short block = world.blocks[xy];
                    int material = block & MASK_MATERIAL;
                    int mtx = material % TEXTURE_COLS;
                    int mty = material / TEXTURE_COLS;
                    boolean filled = (block & MASK_FILLED) != 0;
                    boolean underground = (world.blocks[xy & MASK_UNDERGROUND]) != 0;
                    short construction = (short) (block & MASK_CONSTRUCTION_ANY);
                    if (filled) {
                        for (int i = DDS_BOX_START; i <= DDS_BOX_END; i++) {
                            gl.glNormal3fv(dds[i][4], 0);
                            for (int j = 0; j < 4; j++) {
                                gl.glTexCoord2d((ddstex[j][0] + mtx) / TEXTURE_COLS, (ddstex[j][1] + mty) / TEXTURE_COLS);
                                gl.glVertex3d(x + dds[i][j][0], y + dds[i][j][1], dds[i][j][2]);
                            }
                        }

                    } else if (construction != 0) {
                        switch (construction) {
                            case MASK_CONSTRUCTION_WALL:
                                gl.glNormal3fv(dds[DDS_FLAT][4], 0);
                                for (int j = 0; j < 4; j++) {
                                    gl.glTexCoord2d((ddstex[j][0] + mtx) / TEXTURE_COLS, (ddstex[j][1] + mty) / TEXTURE_COLS);
                                    gl.glVertex3d(x + dds[DDS_FLAT][j][0], y + dds[DDS_FLAT][j][1], dds[DDS_FLAT][j][2]);
                                }
                                for (int i = DDS_BOX_START; i <= DDS_BOX_END; i++) {
                                    gl.glNormal3fv(dds[i][4], 0);
                                    for (int j = 0; j < 4; j++) {
                                        gl.glTexCoord2d((ddstex[j][0] + mtx) / TEXTURE_COLS, (ddstex[j][1] + mty) / TEXTURE_COLS);
                                        gl.glVertex3d(x + 0.1 + 0.8 * dds[i][j][0], y + 0.1 + 0.8 * dds[i][j][1], 0.9 * dds[i][j][2]);
                                    }
                                }
                                break;
                            default:
                                throw new UnsupportedOperationException("Unsupported construction " + construction);
                        }
                    } else {
                        gl.glNormal3fv(dds[DDS_FLAT][4], 0);
                        for (int j = 0; j < 4; j++) {
                            gl.glTexCoord2d((ddstex[j][0] + mtx) / TEXTURE_COLS, (ddstex[j][1] + mty) / TEXTURE_COLS);
                            gl.glVertex3d(x + dds[DDS_FLAT][j][0], y + dds[DDS_FLAT][j][1], dds[DDS_FLAT][j][2]);
                        }/**/
                    }
                    gl.glPopMatrix();
                }
            }

        }
        gl.glEnd();
    }

    private void initGui() {
        canvas.addGLEventListener(new GLEventListener() {

            public void init(GLAutoDrawable drawable) {
                Renderer3D.this.init(drawable);
            }

            public void display(GLAutoDrawable drawable) {
                Renderer3D.this.display(drawable);
            }

            public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
                Renderer3D.this.reshape(drawable, x, y, width, height);
            }

            public void displayChanged(GLAutoDrawable drawable, boolean modeChanged, boolean deviceChanged) {
                Renderer3D.this.displayChanged(drawable, modeChanged, deviceChanged);
            }
        });
        MouseAdapter mouseAdapter = new MouseAdapter() {
            @Override
            public void mouseWheelMoved(MouseWheelEvent e) {
                Renderer3D.this.mouseWheelMoved(e);
            }

            @Override
            public void mouseClicked(MouseEvent e) {
                Renderer3D.this.mouseClicked(e);
            }

            @Override
            public void mouseMoved(MouseEvent e) {
                Renderer3D.this.mouseMoved(e);
            }
        };
        canvas.addMouseListener(mouseAdapter);
        canvas.addMouseMotionListener(mouseAdapter);
        canvas.addMouseWheelListener(mouseAdapter);
        KeyAdapter keyAdapter = new KeyAdapter() {

            @Override
            public void keyPressed(KeyEvent e) {
                Renderer3D.this.keyPressed(e);
            }

            @Override
            public void keyTyped(KeyEvent e) {
                Renderer3D.this.keyTyped(e);
            }

            @Override
            public void keyReleased(KeyEvent e) {
                Renderer3D.this.keyReleased(e);
            }
        };
        canvas.addKeyListener(keyAdapter);
    }

    @SuppressWarnings({"UnusedParameters"})
    private void init(GLAutoDrawable drawable) {
        loadTextures();
    }

    private void loadTextures() {
        try {
            Texture blocktex1 = TextureIO.newTexture(new File("tex.png"), false);
            Texture itemtex1 = TextureIO.newTexture(new File("items.png"), false);
            tiles4items = new TiledTexture(itemtex1, TEXTURE_COLS, TEXTURE_COLS);
            tiles4materials = new TiledTexture(blocktex1, TEXTURE_COLS, TEXTURE_COLS);
            meshes = new Meshes(tiles4materials, tiles4items);
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(-1);
        }
    }

    private void keyTyped(KeyEvent e) {
        switch (e.getKeyChar()) {
            default:
                callback.keyEvent(e);
                break;
        }
    }

    private void keyReleased(KeyEvent e) {
        switch (e.getKeyCode()) {
            case KeyEvent.VK_UP:
            case KeyEvent.VK_DOWN:
                cameraSpeed[1] = 0;
                break;
            case KeyEvent.VK_LEFT:
            case KeyEvent.VK_RIGHT:
                cameraSpeed[0] = 0;
                break;
            case KeyEvent.VK_OPEN_BRACKET:
            case KeyEvent.VK_CLOSE_BRACKET:
                cameraRSpeed[0] = 0;
                cameraRSpeed[2] = 0;
                break;
        }
    }

    private void mouseMoved(MouseEvent e) {
        mouseX = e.getX();
        mouseY = e.getY();
        mouseCellXY = unproject(mouseX, mouseY);
        callback.cellHover(mouseCellXY);
    }

    private void mouseClicked(MouseEvent e) {
        callback.cellClick(mouseCellXY, e);
    }

    private void mouseWheelMoved(MouseWheelEvent e) {
        cameraDistance += e.getPreciseWheelRotation();
        if (cameraDistance < 0.1) cameraDistance = 0.1f;
    }

    private void keyPressed(KeyEvent e) {
        switch (e.getKeyCode()) {
            case KeyEvent.VK_UP:
                cameraSpeed[1] = 1;
                break;
            case KeyEvent.VK_DOWN:
                cameraSpeed[1] = -1;
                break;
            case KeyEvent.VK_RIGHT:
                cameraSpeed[0] = 1;
                break;
            case KeyEvent.VK_LEFT:
                cameraSpeed[0] = -1;
                break;
            case KeyEvent.VK_OPEN_BRACKET:
                if ((e.getModifiers() & KeyEvent.SHIFT_MASK) != 0) {
                    cameraRSpeed[0] = 1;
                } else {
                    cameraRSpeed[2] = 1;
                }
                break;
            case KeyEvent.VK_CLOSE_BRACKET:
                if ((e.getModifiers() & KeyEvent.SHIFT_MASK) != 0) {
                    cameraRSpeed[0] = -1;
                } else {
                    cameraRSpeed[2] = -1;
                }
                break;
        }
    }

    @Override
    public void display() {
        canvas.display();
    }

    @Override
    public void tick(long dt) {
        // move camera
        double CAMERA_SPEED = 5.0;
        double CAMERA_RSPEED = 90.0;
        for (int i = 0; i < 3; i++) {
            cameraTarget[i] += CAMERA_SPEED * cameraSpeed[i] * dt / 1000.0;
            cameraRotation[i] += CAMERA_RSPEED * cameraRSpeed[i] * dt / 1000.0;
        }
    }

    @Override
    public void shown() {
        canvas.requestFocus();
    }

    public GL gl() {
        return gl;
    }

    public void ensureTextureSelected(Texture texture) {
        if (texture != curTex) {
            curTex = texture;
            if (texture != null) {
                ensureEnabled(GL_TEXTURE_2D, true);
                texture.bind();
            } else {
                ensureEnabled(GL_TEXTURE_2D, false);
            }
        }
    }

    public void ensureTextureTile(int tidx, int tw, int th) {
        if (curTexIdx != tidx || curTexWidth != tw || curTexHeight != th) {
            curTexIdx = tidx;
            curTexWidth = tw;
            curTexHeight = th;
            int[] vals = new int[1];
            gl.glGetIntegerv(GL_MATRIX_MODE, vals, 0);
            int matrix_mode = vals[0];
            gl.glMatrixMode(GL_TEXTURE);
            gl.glLoadIdentity();
            gl.glScaled(1.0 / tw, 1.0 / th, 1);
            gl.glTranslated(tidx % tw, tidx / tw, 0);
            gl.glMatrixMode(matrix_mode);
        }
    }

    public TextureReference textureParam(int index) {
        if (index < 0 || index > textureParams.length) return TextureReference.NO_TEXTURE;
        TextureReference result = textureParams[index];
        return (result != null) ? result : TextureReference.NO_TEXTURE;
    }

    public void ensureEnabled(int glParam, boolean enabled) {
        if (curGlParams.contains(glParam) && !enabled) {
            curGlParams.remove(glParam);
            gl.glDisable(glParam);
        } else if (!curGlParams.contains(glParam) && enabled) {
            curGlParams.add(glParam);
            gl.glEnable(glParam);
        }
    }

    public void ensureClientEnabled(int glParam, boolean enabled) {
        if (curGlClientParams.contains(glParam) && !enabled) {
            curGlClientParams.remove(glParam);
            gl.glDisableClientState(glParam);
        } else if (!curGlParams.contains(glParam) && enabled) {
            curGlClientParams.add(glParam);
            gl.glEnableClientState(glParam);
        }
    }
}